/*
 * Copyright (c) 2011, Yawning <yawninglol at gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   * Neither the name of the Mew Developers nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This is based off a hack that Mihir worked on to automatically find ideal stat
 * targets for reforging. The difference between his code and mine is that mine
 * is generic and somewhat cleaner.
 */

package yawning.mew.GeneticOptimizer;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;

import org.apache.commons.math.genetics.ElitisticListPopulation;
import org.apache.commons.math.genetics.GeneticAlgorithm;
import org.apache.commons.math.genetics.Population;
import org.apache.commons.math.genetics.StoppingCondition;
import org.apache.commons.math.genetics.TournamentSelection;

import yawning.mew.Mew;
import yawning.mew.MewPRNG;
import yawning.mew.MewProperty;
import yawning.mew.PropertiesBase;
import yawning.mew.Report;
import yawning.mew.Report.Stat;
import yawning.mew.character.Toon;
import yawning.mew.character.ToonToonBuilder;
import yawning.mew.target.Target;

public class Optimizer {
	// Runtime tunable variables.
	// TODO: Come up with sane values for the algorithm parameters.
	private static final MewProperty STAT_SHIFT_MIN = new MewProperty("Optimizer.StatShiftMin", 0.4);		// Reduce up to x% of base max.
	private static final MewProperty CROSSOVER_RATE = new MewProperty("Optimizer.CrossoverRate", 0.94);
	private static final MewProperty HIT_EXP_RATE = new MewProperty("Optimizer.HitExpRate", 0.05);			// Avoid up to x% of hit<->exp shifts.
	private static final MewProperty MULT_STAT_RATE = new MewProperty("Optimizer.MultiStatRate", 0.25);		// After shuffling a stat, x% chance to shuffle another.
	private static final MewProperty MUTATION_RATE = new MewProperty("Optimizer.MutationRate", 0.1);
	private static final MewProperty NUMBER_GENERATIONS = new MewProperty("Optimizer.NumberGenerations", 10000);
	private static final MewProperty POPULATION = new MewProperty("Optimizer.Population", 1000);
	private static final MewProperty ELITISM_RATE = new MewProperty("Optimizer.ElitismRate", 0.01);
	private static final MewProperty TOURNAMENT_RATIO = new MewProperty("Optimizer.TournamentRatio", 0.5);
	private static final MewProperty STAGNATION_HALT = new MewProperty("Optimizer.StagnationHalt", 100);

	// Candidate stats to optimize.
	public enum CandidateStat {
		SPIRIT,
		DODGE_RATING,
		PARRY_RATING,
		HASTE_RATING,
		CRIT_RATING,
		HIT_RATING,
		EXPERTISE_RATING,
		MASTERY_RATING
	}

	private class OptimizerReport extends Report {
		String mString;

		OptimizerReport(String s) {
			mString = s;
		}

		@Override
		public String toString() {
			return mString;
		}
	}

	public Optimizer() {
		// Nothing to initialize.
	}

	Mew mMew;
	MewPRNG mRng;
	ToonToonBuilder mToonToonBuilder;

	Toon mBaseToon;
	Target mTarget;
	PropertiesBase mConfig;
	Stat mFitnessStat;
	List<CandidateStat> mCandidateStats;
	HashMap<CandidateStat,Field> mStatToonFieldMap;
	Population mPopulation;
	int mStagnationHalt;
	int mNrStats;
	int mStatPool;

	public Report compute(Mew mew, Toon toon, Target target, PropertiesBase config, Stat fitnessStat, List<CandidateStat> candidateStats) {
		mToonToonBuilder = new ToonToonBuilder();
		mRng = new MewPRNG("org.apache.commons.math.random.Well19937c", (int) System.currentTimeMillis());

		mMew = mew;
		mBaseToon = mToonToonBuilder.build(toon);
		mTarget = target;
		mConfig = config;
		mFitnessStat = fitnessStat;
		mCandidateStats = candidateStats;
		mNrStats = candidateStats.size();
		mStagnationHalt = config.getInt(STAGNATION_HALT);

		mStatToonFieldMap = new HashMap<CandidateStat,Field>();
		try {
			mStatToonFieldMap.put(CandidateStat.SPIRIT, Toon.class.getField("spirit"));
			mStatToonFieldMap.put(CandidateStat.DODGE_RATING, Toon.class.getField("mDodgeRating"));
			mStatToonFieldMap.put(CandidateStat.PARRY_RATING, Toon.class.getField("mParryRating"));
			mStatToonFieldMap.put(CandidateStat.HASTE_RATING, Toon.class.getField("hasteRating"));
			mStatToonFieldMap.put(CandidateStat.CRIT_RATING, Toon.class.getField("critRating"));
			mStatToonFieldMap.put(CandidateStat.HIT_RATING, Toon.class.getField("hitRating"));
			mStatToonFieldMap.put(CandidateStat.EXPERTISE_RATING, Toon.class.getField("expertiseRating"));
			mStatToonFieldMap.put(CandidateStat.MASTERY_RATING, Toon.class.getField("masteryRating"));
		} catch (Exception e) {
			throw new RuntimeException(e);
		}

		// Determine the amount of reallocatable stats and create a toon instance with that pool stripped out.
		int usersProfile[] = new int[mNrStats];
		double statMinRatio = config.getDouble(STAT_SHIFT_MIN);
		for (int i = 0; i < mNrStats; i++) {
			Field f = null;
			CandidateStat stat = mCandidateStats.get(i);

			try {
				f = mStatToonFieldMap.get(stat);

				int statVal = f.getInt(toon);
				int minReqStat = (int) Math.floor(statVal - statVal * statMinRatio);
				if (statMinRatio == 1.0) minReqStat = 0;

				System.out.println("Stat: " + stat + " MinRequired: " + minReqStat);

				mStatPool += statVal - minReqStat;
				usersProfile[i] = statVal - minReqStat;
				f.setInt(mBaseToon, minReqStat);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
		System.out.println();

		// Initialize the genetic algorithm.
		GeneticAlgorithm ga = new GeneticAlgorithm(
				new MutatingCrossover(this, config.getDouble(MULT_STAT_RATE), config.getDouble(HIT_EXP_RATE)),
				config.getDouble(CROSSOVER_RATE),
				new ReplacementMutation(),
				config.getDouble(MUTATION_RATE),
				new TournamentSelection((int) Math.round(config.getInt(POPULATION) * config.getDouble(TOURNAMENT_RATIO)))
		);
		StoppingCondition stopCond = new GOStoppingCondition(this, config.getInt(NUMBER_GENERATIONS));

		// Create and populate our initial population.
		Population initial = new ElitisticListPopulation(config.getInt(POPULATION), config.getDouble(ELITISM_RATE));
		GOChromosome userChromosome = new GOChromosome(this, usersProfile);	// Give the user token representation.
		initial.addChromosome(userChromosome);
		for (int i = 0; i < mNrStats; i++) {
			// May as well try shifting all the stats into one of them.
			int statVec[] = new int[mNrStats];
			statVec[i] = mStatPool;
			initial.addChromosome(new GOChromosome(this, statVec));
		}
		int pop = config.getInt(POPULATION);
		for (int i = 1 + mNrStats; i < pop; i++) {
			initial.addChromosome(userChromosome.newRandomChromosome());
		}

		// Evolve into a superior form.
		mPopulation = ga.evolve(initial, stopCond);

		//
		// Ok, my final population has those that are good enough to survive the selection process.
		// TODO: Since I sim at a lower resolution than needed for accuracy, I should recalculate with the iteration count jacked up.
		//

		//
		// HACK HACK HACK HACK: Generate a report based on the coarse resolution dataset.
		//
		GOChromosome bestFinal = (GOChromosome) mPopulation.getFittestChromosome();
		StringBuilder s = new StringBuilder();
		s.append("Optimizer results:\n");
		s.append("Fitness: " + Report.round(bestFinal.getFitness())).append('\n');
		for (CandidateStat stat: mCandidateStats) {
			switch (stat) {
			case DODGE_RATING: s.append("Dodge Rating: " + bestFinal.mDodgeRating); break;
			case PARRY_RATING: s.append("Parry Rating: " + bestFinal.mParryRating).append('\n'); break;
			case HASTE_RATING: s.append("Haste Rating: " + bestFinal.mHasteRating).append('\n'); break;
			case CRIT_RATING: s.append("Crit Rating: " + bestFinal.mCritRating).append('\n'); break;
			case HIT_RATING: s.append("Hit Rating: " + bestFinal.mHitRating).append('\n'); break;
			case EXPERTISE_RATING: s.append("Expertise Rating: " + bestFinal.mExpertiseRating).append('\n'); break;
			case MASTERY_RATING: s.append("Mastery Rating: " + bestFinal.mMasteryRating).append('\n'); break;
			}
		}
		s.append('\n');

		OptimizerReport report = new OptimizerReport(s.toString());

		return report;
	}
}
